﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.Hosting;
using System.Web.UI.WebControls;
using System.Reflection;
using System.Web.Mvc;
using System.IO;
using Newtonsoft.Json;

[assembly: WebResource("jMVCHelper.jMVCplusJson.js", "application/x-javascript")]
namespace jMVCHelper
{
    public static class jMVC {
        private static JsonSerializationHelper s_SerializationHelper = new JsonSerializationHelper();
        private static object s_ScriptRegistrationKey = new object();

        /// <summary>
        /// Renders the HTML necessary to place a jMVC panel in your page
        /// </summary>
        /// <param name="viewPage">The host page</param>
        /// <param name="name">The HTML name of the control (using which you can retrieve the submitted data when the form is posted)</param>
        /// <param name="viewData">A JSON-serializable object which acts as the ViewData inside the MVC panel control</param>
        /// <param name="viewUrl">The URL of your jMVC view template (can be application-relative)</param>
        /// <param name="jsControllerObject">Optional: a Javascript object identifier which will be made available to your view template as 'controller'.</param>
        public static string MVCPanel(ViewPage viewPage, string name, object viewData, string viewUrl)
        {
            return MVCPanel(viewPage, name, viewData, viewUrl, null);
        }

        /// <summary>
        /// Renders the HTML necessary to place a jMVC panel in your page
        /// </summary>
        /// <param name="viewPage">The host page</param>
        /// <param name="name">The HTML name of the control (using which you can retrieve the submitted data when the form is posted)</param>
        /// <param name="viewData">A JSON-serializable object which acts as the ViewData inside the MVC panel control</param>
        /// <param name="viewUrl">The URL of your jMVC view template (can be application-relative)</param>
        /// <param name="jsControllerObject">Optional: a Javascript object identifier which will be made available to your view template as 'controller'.</param>
        public static string MVCPanel(ViewPage viewPage, string name, object viewData, string viewUrl, string jsControllerObject)
        {
            StringBuilder response = new StringBuilder();

            // Add a script reference if needed (since viewPage.ClientScript.RegisterClientScriptResource() doesn't work with MVC)
            if (!viewPage.ViewContext.HttpContext.Items.Contains(s_ScriptRegistrationKey))
            {
                viewPage.ViewContext.HttpContext.Items.Add(s_ScriptRegistrationKey, true);
                response.AppendFormat("<script src='{0}'></script>", viewPage.ClientScript.GetWebResourceUrl(typeof(jMVC), "jMVCHelper.jMVCplusJson.js"));
                response.AppendLine();
            }

            // Render the viewData as JSON in a hidden INPUT
            string jsonData = HttpUtility.HtmlEncode(s_SerializationHelper.Serialize(viewData));
            response.AppendFormat("<input type='hidden' id='{0}' name='{0}' value='{1}'/>", name, jsonData);
            response.AppendLine();

            // Add a DIV to hold the rendered UI
            string viewDivID = name + "_view";
            response.AppendFormat("<div id='{0}'></div>", viewDivID);

            // Create a JS object to hold the model data
            string viewAbsoluteUrl = GetViewAbsoluteURL(viewUrl);
            string modelObjectID = name + "_modelobject";
            response.AppendLine("<script language='Javascript'>");
            response.AppendFormat("var {0} = eval('('+document.getElementById('{1}').value+')');" + Environment.NewLine, modelObjectID, name);
            
            // Now invoke the jMVC client-side engine, supplying the view and ID of the model textarea
            response.Append("jMVC.addLoadEvent(function() {"); // Unfortunately you can't use viewPage.ClientScript.RegisterStartupScript() with MVC
            response.AppendFormat("new jMVC({0}, jMVC.prototype.LoadStringFromURL(\"{1}\"), \"{2}\", \"{3}\", {4});" + Environment.NewLine, modelObjectID, viewAbsoluteUrl, viewDivID, name, string.IsNullOrEmpty(jsControllerObject) ? "null" : jsControllerObject);
            response.Append("});");
            response.AppendLine("</script>");

            return response.ToString();
        }

        /// <summary>
        /// Supply your own JsonConverter to override the JSON serialization style
        /// for any Type of your choosing. You should only register each converter
        /// once (during application initialization)
        /// </summary>
        public static void RegisterJsonConverter(JsonConverter converter)
        {
            s_SerializationHelper.RegisterJsonConverter(converter);
        }

        private static string GetViewAbsoluteURL(string viewUrl)
        {
            // Convert the view URL into something the browser will understand
            string viewAbsoluteURL;
            if ((!System.Uri.IsWellFormedUriString(viewUrl, UriKind.Absolute)) && (VirtualPathUtility.IsAppRelative(viewUrl)))
                viewAbsoluteURL = VirtualPathUtility.ToAbsolute(viewUrl);
            else
                viewAbsoluteURL = viewUrl;
            return viewAbsoluteURL;
        }

        #region Parsing the JSON data sent in a request
        private static T ReadJsonFromRequest<T>(JsonReader reader, string objectPath)
        {
            if (objectPath == null)
                return (T)s_SerializationHelper.Deserialize(reader, typeof(T));
            else
            {
                // Recursively try to find the requested object
                reader.Read();
                while (reader.Read())
                {
                    switch (reader.TokenType)
                    {
                        case JsonToken.PropertyName:
                            if (objectPath == (string)reader.Value)
                                // Found it
                                return ReadJsonFromRequest<T>(reader, null); 
                            else if (objectPath.StartsWith((string)reader.Value + "."))
                                // Should be a descendent of this one
                                return ReadJsonFromRequest<T>(reader, objectPath.Substring(((string)reader.Value).Length + 1));
                            break;
                        default:
                            throw new JsonSerializationException("Didn't find the expected prefix: " + objectPath);
                    }
                }
            }

            throw new JsonSerializationException("Didn't find the expected prefix: " + objectPath);
        }

        /// <summary>
        /// Deserializes the JSON string posted to the page as a loosely-typed JavascriptObject
        /// </summary>
        /// <param name="htmlName">The key into the request data collection</param>
        public static JavaScriptObject ReadJsonFromRequest(this Controller controller, string htmlName)
        {
            return ReadJsonFromRequest<JavaScriptObject>(controller, htmlName);
        }

        /// <summary>
        /// Deserializes the JSON string posted to the page
        /// </summary>
        /// <typeparam name="T">The type of .NET object expected</typeparam>
        /// <param name="htmlName">The key into the request data collection</param>
        public static T ReadJsonFromRequest<T>(this Controller controller, string htmlName)
        {
            return ReadJsonFromRequest<T>(controller, htmlName, null);
        }

        /// <summary>
        /// Deserializes the JSON string posted to the page
        /// </summary>
        /// <typeparam name="T">The type of .NET object expected</typeparam>
        /// <param name="htmlName">The key into the request data collection</param>
        /// <param name="jsonObjectPath">Rather than deserializing the entire JSON string, you can extract an individual object by specifying the path</param>
        public static T ReadJsonFromRequest<T>(this Controller controller, string htmlName, string jsonObjectPath)
        {
            StringReader sr = new System.IO.StringReader(controller.Request[htmlName]);
            JsonReader reader = new JsonReader(sr);

            return ReadJsonFromRequest<T>(reader, jsonObjectPath);
        }
        #endregion
    }
}
